/** * Copyright (C) 2011 BonitaSoft S.A. * BonitaSoft, 32 rue Gustave Eiffel - 38000 Grenoble * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2.0 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.bonitasoft.console.common.server.auth.impl.jaas; import java.security.Principal; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.security.auth.Destroyable; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; import org.bonitasoft.console.common.server.login.credentials.LoginDatastore; import org.bonitasoft.engine.exception.BonitaException; import org.bonitasoft.engine.session.APISession; /** * This {@link LoginModule} is used to verify a user identity against Bonita authentication service. * * @author Qixiang Zhang */ public class ConsoleIdentityLoginModule implements LoginModule { /** * User name prompt */ private static final String NAME_PROMPT = "Name: "; /** * User password prompt */ private static final String PASSWORD_PROMPT = "Password: "; /** * javax security auth login password */ protected static final String JAVAX_SECURITY_AUTH_LOGIN_PASSWORD = "javax.security.auth.login.password"; /** * javax security auth login name */ protected static final String JAVAX_SECURITY_AUTH_LOGIN_NAME = "javax.security.auth.login.name"; /** * Property key for the debug flag. Defined to be "debug". * * Property Value. If set, should be either "true" or "false". Default is * "false". */ public static final String DEBUG_OPTION_NAME = "debug"; /** * The subject to be authenticated */ private Subject subject = null; /** * A CallbackHandler for communicating with the end user (prompting * for usernames and passwords, for example) */ private CallbackHandler callbackHandler = null; /** * State shared with other configured LoginModules. */ private Map<String, Object> sharedState; /** * Debug flag */ private boolean debug = false; /** * Principal id */ private String id; /** * Initialize this LoginModule. This method is called by the LoginContext * after this LoginModule has been instantiated. The purpose of this method is * to initialize this LoginModule with the relevant information. If this * LoginModule does not understand any of the data stored in sharedState or * options parameters, they can be ignored. * * @param subject * the Subject to be authenticated. * @param callbackHandler * a CallbackHandler for communicating with the end user (prompting * for usernames and passwords, for example). * @param sharedState * state shared with other configured LoginModules. * @param options * options specified in the login Configuration for this particular * LoginModule. */ @Override @SuppressWarnings("unchecked") public void initialize(final Subject subject, final CallbackHandler callbackHandler, final Map<String, ?> sharedState, final Map<String, ?> options) { this.subject = subject; this.callbackHandler = callbackHandler; this.sharedState = (Map<String, Object>) sharedState; final String debugFlag = (String) options.get(DEBUG_OPTION_NAME); if (debugFlag != null) { this.debug = Boolean.valueOf(debugFlag); } } /** * Method to authenticate a Subject (phase 1). The implementation of this * method authenticates a Subject. For example, it may prompt for Subject * information such as a username and password and then attempt to verify the * password. This method saves the result of the authentication attempt as * private state within the LoginModule. * * @return true if the authentication succeeded, or false if this LoginModule * should be ignored. * @throws LoginException * if the authentication fails */ @Override public boolean login() throws LoginException { if (this.debug) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] login() - preparing - step 1"); } try { final Map<String, Object> loggingsArgs = getSharedState(); final Map<String, Callback> callbacks = getPromptCallbacks(loggingsArgs); if (!callbacks.isEmpty()) { if (this.debug) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] login() - callback - step 2"); } this.callbackHandler.handle(callbacks.values().toArray(new Callback[0])); adjustLoggingsArgs(callbacks, loggingsArgs); } if (isDebug()) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] login() - authenticating - step 3"); } final APISession aAPISession = (loggingsArgs.containsKey(JAVAX_SECURITY_AUTH_LOGIN_NAME)) ? doLogin(loggingsArgs) : null; if (isDebug()) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] login() - storing data - step 4"); } if (aAPISession != null) { this.id = (String) getSharedState().get(JAVAX_SECURITY_AUTH_LOGIN_NAME); } if (isDebug()) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] login() - returning - step 5"); } if (this.id == null) { throw new FailedLoginException("id is null"); } return true; } catch (final Exception e) { e.printStackTrace(); final LoginException le = new LoginException(); le.initCause(e); throw le; } } /** * Log user in * * @param loggingsArgs * @return * @throws BonitaException */ protected APISession doLogin(final Map<String, Object> loggingsArgs) throws BonitaException { final LoginDatastore loginDatastore = new LoginDatastore(); return loginDatastore.login(String.valueOf(loggingsArgs.get(JAVAX_SECURITY_AUTH_LOGIN_NAME)), String.valueOf(loggingsArgs.get(JAVAX_SECURITY_AUTH_LOGIN_PASSWORD))); } /** * Get callback to prompt authentication to user * * @param loggingsArgs * @return */ protected Map<String, Callback> getPromptCallbacks(final Map<String, Object> loggingsArgs) { final Map<String, Callback> callbacks = new HashMap<String, Callback>(); // login if (!loggingsArgs.containsKey(JAVAX_SECURITY_AUTH_LOGIN_NAME)) { callbacks.put(NAME_PROMPT, new NameCallback(NAME_PROMPT)); } // password if (!loggingsArgs.containsKey(JAVAX_SECURITY_AUTH_LOGIN_PASSWORD)) { callbacks.put(PASSWORD_PROMPT, new PasswordCallback(PASSWORD_PROMPT, false)); } return callbacks; } /** * Adjust loggings arguments depending on callbacks results * * @param callbacks * @param loggingsArgs */ protected void adjustLoggingsArgs(final Map<String, Callback> callbacks, final Map<String, Object> loggingsArgs) { if (!loggingsArgs.containsKey(JAVAX_SECURITY_AUTH_LOGIN_NAME)) { // update name if (callbacks.get(NAME_PROMPT) instanceof NameCallback) { loggingsArgs.put(JAVAX_SECURITY_AUTH_LOGIN_NAME, ((NameCallback) callbacks.get(NAME_PROMPT)).getName()); } } if (!loggingsArgs.containsKey(JAVAX_SECURITY_AUTH_LOGIN_PASSWORD)) { // update password if (callbacks.get(PASSWORD_PROMPT) instanceof PasswordCallback) { PasswordCallback pwdCallback = ((PasswordCallback) callbacks.get(PASSWORD_PROMPT)); loggingsArgs.put(JAVAX_SECURITY_AUTH_LOGIN_PASSWORD, String.valueOf(pwdCallback.getPassword())); pwdCallback.clearPassword(); } } } /** * Method to commit the authentication process (phase 2). This method is * called if the LoginContext's overall authentication succeeded (the relevant * REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules succeeded). If * this LoginModule's own authentication attempt succeeded (checked by * retrieving the private state saved by the login method), then this method * associates relevant Principals and Credentials with the Subject located in * the LoginModule. If this LoginModule's own authentication attempted failed, * then this method removes/destroys any state that was originally saved. * * @return true if this method succeeded, or false if this LoginModule should * be ignored. * @throws LoginException * if the commit fails */ @Override public boolean commit() throws LoginException { if (this.id == null) { throw new FailedLoginException("id is null"); } final Set<Principal> principals = this.subject.getPrincipals(); principals.add(new ConsolePrincipal(this.id)); return true; } /** * Method to abort the authentication process (phase 2). This method is called * if the LoginContext's overall authentication failed. (the relevant * REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules did not succeed). * If this LoginModule's own authentication attempt succeeded (checked by * retrieving the private state saved by the login method), then this method * cleans up any state that was originally saved. * * @return true if this method succeeded, or false if this LoginModule should * be ignored. * @throws LoginException * if the abort fails */ @Override public boolean abort() throws LoginException { if (this.debug) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] abort()"); } if (this.id == null) { return false; } this.subject = null; this.id = null; return true; } /** * Method which logs out a Subject. An implementation of this method might * remove/destroy a Subject's Principals and Credentials. * * @return true if this method succeeded, or false if this LoginModule should * be ignored. * @throws LoginException * if the logout fails */ @Override public boolean logout() throws LoginException { if (this.id != null) { if (this.debug) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] logout() - removing principals"); } // Remove only principals added by our commit method final Set<Principal> principals = new HashSet<Principal>(this.subject.getPrincipals()); for (final Principal p : principals) { if (p instanceof ConsolePrincipal) { if (this.debug) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] logout() - removing principal: " + p); } this.subject.getPrincipals().remove(p); } } if (this.debug) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] logout() - destroying/removing credentials"); } // Remove/destroy only credentials added by our commit method final Set<Object> credentials = new HashSet<Object>(this.subject.getPublicCredentials()); for (final Object o : credentials) { if (o instanceof Destroyable) { if (this.debug) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] logout() - destroying credential: " + o); } // Bug: only from this module !! // ((Destroyable) o).destroy(); } if (!this.subject.isReadOnly()) { if (this.debug) { System.err.println("[" + ConsoleIdentityLoginModule.class.getName() + "] logout() - removing credential: " + o); } this.subject.getPublicCredentials().remove(o); } } } return true; } protected Map<String, Object> getSharedState() { return sharedState; } protected boolean isDebug() { return debug; } }